import asyncio
import datetime
import logging
import pybotters

import logic
import util.candlestick
import util.depth
import util.pair
import util.ticker


PUBLIC_API_ENTRYPOINT  = "https://public.bitbank.cc"
PRIVATE_API_ENTRYPOINT = "https://api.bitbank.cc/v1"

ORDER_UNITS = {
  "btc_jpy": 0.0001,
  "eth_jpy": 0.0001,
}
PRICE_UNITS = {
  "btc_jpy": 1,
  "eth_jpy": 1,
}

logger = logging.getLogger(__name__)


_k = asyncio.Lock()

async def init(cli):
  #pair = Pair(cli, "mona_jpy")
  #logic.Hige(
  #    logging.getLogger("bitbank/mona/Hige"),
  #    pair, 4, 0.2)

  pair = Pair(cli, "btc_jpy")
  logic.EMA_Chicken(
      logging.getLogger("bitbank/btc/EMA_Chicken"),
      pair, pair.candlestick_1m, 0.001, 0.005)

  pair = Pair(cli, "eth_jpy")
  logic.MM(
      logging.getLogger("bitbank/eth/MM"),
      pair, 0.01, 1, 0.15)
  logic.EMA_Chicken(
      logging.getLogger("bitbank/eth/EMA_Chicken"),
      pair, pair.candlestick_1m, 0.01, 0.005)


class Pair(util.pair.Pair):
  def __init__(self, cli, name):
    super().__init__(name)
    self._cli   = cli
    self._store = pybotters.bitbankDataStore()

    self.ticker = Ticker(self._store, name)

    self.order_unit = ORDER_UNITS[name]
    self.price_unit = PRICE_UNITS[name]

    #self.candlestick_1s = Candlestick(cli, self.ticker, None, 1)
    self.candlestick_1m = Candlestick(cli, self.ticker, "1min", 60)
    #self.candlestick_1h = Candlestick(cli, self.ticker, "1hour", 60*60)

    self.depth = Depth(self._store, self.order_unit)

    asyncio.create_task(cli.ws_connect(
      'wss://stream.bitbank.cc/socket.io/?EIO=3&transport=websocket',
      send_str=[
        f'42["join-room","ticker_{name}"]',
        f'42["join-room","transactions_{name}"]',
        f'42["join-room","depth_whole_{name}"]',
        f'42["join-room","depth_diff_{name}"]',
      ],
      hdlr_str = self._store.onmessage,
    ))

  async def _order(self, amount, side, type, trigger = None, price = None, post_only = None):
    data = {
      "pair"  : self.name,
      "amount": str(amount),
      "side"  : side,
      "type"  : type,
    }
    if price     is not None: data["price"]         = price
    if trigger   is not None: data["trigger_price"] = trigger
    if post_only is not None: data["post_only"]     = post_only
    async with _k:
      res = await self._cli.post(PRIVATE_API_ENTRYPOINT+"/user/spot/order", data=data)
      res = await res.json()
    _check_response(res)
    order = Order(self._cli, self, res["data"]["order_id"])
    order.amount = amount
    order.price  = price
    return order

  def buy_market(self, amount):
    return self._order(amount, "buy", "market")

  def sell_market(self, amount):
    return self._order(amount, "sell", "market")

  def buy_limit(self, amount, price, post_only = False):
    return self._order(amount, "buy", "limit", None, price, post_only)

  def sell_limit(self, amount, price, post_only = False):
    return self._order(amount, "sell", "limit", None, price, post_only)

  def buy_stop(self, amount, trigger):
    return self._order(amount, "buy", "stop", trigger)

  def sell_stop(self, amount, trigger):
    return self._order(amount, "sell", "stop", trigger)

  def buy_stop_limit(self, amount, trigger, price):
    return self._order(amount, "buy", "stop", trigger, price)

  def sell_stop_limit(self, amount, trigger, price):
    return self._order(amount, "sell", "stop", trigger, price)


class Order(util.pair.Order):
  def __init__(self, cli, pair, id):
    super().__init__()
    self._cli  = cli
    self._pair = pair
    self._id   = id

  async def update(self):
    if self.done: return
    for i in range(5):
      async with _k:
        res = await self._cli.get(PRIVATE_API_ENTRYPOINT+"/user/spot/order", params={
          "pair": self._pair.name,
          "order_id": self._id,
        })
        res = await res.json()
      if res["success"] == 1: break

      # sometimes fails without a reason, so it does retry :(
      if res["data"]["code"] != 50009 and str(res["data"]["code"]) in ERROR_CODES:
        _check_response(res)
      await asyncio.sleep(1)
    if res["success"] != 1: raise Exception("failed to update")

    res = res["data"]
    self.amount = float(res["start_amount"])
    self.remain = float(res["remaining_amount"])
    self.price  = float(res["average_price"])
    self.done   =  \
      res["status"] == "FULLY_FILLED" or  \
      res["status"] == "CANCELED_UNFILLED" or  \
      res["status"] == "CANCELED_PARTIALLY_FILLED"

  async def cancel(self):
    for i in range(5):
      async with _k:
        res = await self._cli.post(PRIVATE_API_ENTRYPOINT+"/user/spot/cancel_order", data={
          "pair": self._pair.name,
          "order_id": self._id,
        })
        res = await res.json()
      if res["success"] == 1:
        res = res["data"]
        self.amount = float(res["start_amount"])
        self.remain = float(res["remaining_amount"])
        self.price  = float(res["average_price"])
        self.done   = True
        return

      code = res["data"]["code"]
      if code == 50027:
        await self.update()
        return

      # sometimes fails without a reason, so it does retry :(
      retry =  \
        code == 50009 or  \
        str(code) not in ERROR_CODES

      if not retry:
        _check_response(res)
      await asyncio.sleep(1)
    raise Exception("failed to cancel")


class Ticker(util.ticker.Ticker):
  def __init__(self, store, pair):
    super().__init__(pair)
    self._store = store
    asyncio.create_task(self._main())

  async def _main(self):
    while True:
      await self._store.ticker.wait()
      ticker = self._store.ticker.find()
      for i in range(len(ticker)):
        if ticker[i]["pair"] == self.pair:
          self.price  = float(ticker[i]["last"])
          self.sell   = float(ticker[i]["sell"])
          self.buy    = float(ticker[i]["buy"])
          self.volume = float(ticker[i]["vol"])
          self.ts     = int(ticker[i]["timestamp"])
          self._event.set()
          self._event.clear()
          break


class Candlestick(util.candlestick.Candlestick):
  def __init__(self, cli, ticker, unit, interval):
    super().__init__(ticker, interval)
    self._cli  = cli
    self._unit = unit

  async def _fetch(self, date, depth = 0, rem = util.candlestick.MAX_CANDLES):
    if self._unit is None:
      return []

    long_term =  \
      self._unit == "4hour" or  \
      self._unit == "8hour" or  \
      self._unit == "12hour" or  \
      self._unit == "1day" or  \
      self._unit == "1week" or  \
      self._unit == "1month"
    if long_term:
      strdate = date.strftime("%Y")
    else:
      strdate = date.strftime("%Y%m%d")

    pair = self._ticker.pair
    async with _k:
      data = await self._cli.get(PUBLIC_API_ENTRYPOINT+f"/{pair}/candlestick/{self._unit}/{strdate}")
      data = await data.json()
    _check_response(data)
    data = data["data"]["candlestick"][0]["ohlcv"]

    first_index = 0
    if depth == 0: first_index = 1

    ret = []
    for i in range(first_index, len(data)):
      ret.append([
        float(data[i][0]),
        float(data[i][1]),
        float(data[i][2]),
        float(data[i][3]),
      ])

    if len(ret) < rem:
      back = datetime.timedelta(days=1)
      if long_term:
        back = datetime.timedelta(years=1)
      ret = [*ret, *await self._fetch(date-back, depth+1, rem-len(ret))]
    return ret


class Depth(util.depth.Depth):
  def __init__(self, store, order_unit):
    super().__init__(order_unit)
    self._store = store
    asyncio.create_task(self._main())

  async def _main(self):
    while True:
      await self._store.depth.wait()
      items = self._store.depth.sorted()
      asks = items["asks"]
      bids = items["bids"]

      self.asks = []
      for i in range(len(asks)):
        self.asks.append([float(asks[i][0]), float(asks[i][1])])
      self.bids = []
      for i in range(len(bids)):
        self.bids.append([float(bids[i][0]), float(bids[i][1])])
      self._event.set()
      self._event.clear()


def _check_response(res):
  if res["success"] == 1:
    return

  code = str(res["data"]["code"])
  if code in ERROR_CODES:
    raise Exception(ERROR_CODES[code])
  raise Exception("unknown error")

ERROR_CODES = {
    '10000': 'URLが存在しません',
    '10001': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
    '10002': '不正なJSON形式です。送信内容をご確認下さい',
    '10003': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
    '10005': 'タイムアウトエラーが発生しました。しばらく間をおいて再度実行して下さい',
    '20001': 'API認証に失敗しました',
    '20002': 'APIキーが不正です',
    '20003': 'APIキーが存在しません',
    '20004': 'API Nonceが存在しません',
    '20005': 'APIシグネチャが存在しません',
    '20011': '２段階認証に失敗しました',
    '20014': 'SMS認証に失敗しました',
    '20023': '２段階認証コードを入力して下さい',
    '20024': 'SMS認証コードを入力して下さい',
    '20025': '２段階認証コードとSMS認証コードを入力して下さい',
    '20026': '一定回数以上２段階認証に失敗したためロックしました。60秒待ってから再度お試しください',
    '30001': '注文数量を指定して下さい',
    '30006': '注文IDを指定して下さい',
    '30007': '注文ID配列を指定して下さい',
    '30009': '銘柄を指定して下さい',
    '30012': '注文価格を指定して下さい',
    '30013': '売買どちらかを指定して下さい',
    '30015': '注文タイプを指定して下さい',
    '30016': 'アセット名を指定して下さい',
    '30019': 'uuidを指定して下さい',
    '30039': '出金額を指定して下さい',
    '30101': 'トリガー価格を指定してください',
    '40001': '注文数量が不正です',
    '40006': 'count値が不正です',
    '40007': '終了時期が不正です',
    '40008': 'end_id値が不正です',
    '40009': 'from_id値が不正です',
    '40013': '注文IDが不正です',
    '40014': '注文ID配列が不正です',
    '40015': '指定された注文が多すぎます',
    '40017': '銘柄名が不正です',
    '40020': '注文価格が不正です',
    '40021': '売買区分が不正です',
    '40022': '開始時期が不正です',
    '40024': '注文タイプが不正です',
    '40025': 'アセット名が不正です',
    '40028': 'uuidが不正です',
    '40048': '出金額が不正です',
    '40112': 'トリガー価格が不正です',
    '40113': 'post_only値が不正です',
    '40114': 'Post Onlyはご指定の注文タイプでは指定できません',
    '50003': '現在、このアカウントはご指定の操作を実行できない状態となっております。サポートにお問い合わせ下さい',
    '50004': '現在、このアカウントは仮登録の状態となっております。アカウント登録完了後、再度お試し下さい',
    '50005': '現在、このアカウントはロックされております。サポートにお問い合わせ下さい',
    '50006': '現在、このアカウントはロックされております。サポートにお問い合わせ下さい',
    '50008': 'ユーザの本人確認が完了していません',
    '50009': 'ご指定の注文は存在しません',
    '50010': 'ご指定の注文はキャンセルできません',
    '50011': 'APIが見つかりません',
    '50026': 'ご指定の注文は既にキャンセル済みです',
    '50027': 'ご指定の注文は既に約定済みです',
    '60001': '保有数量が不足しています',
    '60002': '成行買い注文の数量上限を上回っています',
    '60003': '指定した数量が制限を超えています',
    '60004': '指定した数量がしきい値を下回っています',
    '60005': '指定した価格が上限を上回っています',
    '60006': '指定した価格が下限を下回っています',
    '60011': '同時発注制限件数(30件)を上回っています',
    '60016': '指定したトリガー価格が上限を上回っています',
    '70001': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
    '70002': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
    '70003': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
    '70004': '現在取引停止中のため、注文を承ることができません',
    '70005': '現在買い注文停止中のため、注文を承ることができません',
    '70006': '現在売り注文停止中のため、注文を承ることができません',
    '70009': 'ただいま成行注文を一時的に制限しています。指値注文をご利用ください',
    '70010': 'ただいまシステム負荷が高まっているため、最小注文数量を一時的に引き上げています',
    '70011': 'ただいまリクエストが混雑してます。しばらく時間を空けてから再度リクエストをお願いします',
    '70012': 'システムエラーが発生しました。サポートにお問い合わせ下さい',
    '70013': 'ただいまシステム負荷が高まっているため、注文および注文キャンセルを一時的に制限しています',
    '70014': 'ただいまシステム負荷が高まっているため、出金申請および出金申請キャンセルを一時的に制限しています',
    '70015': 'ただいまシステム負荷が高まっているため、貸出申請および貸出申請キャンセルを一時的に制限しています',
    '70016': '貸出申請および貸出申請キャンセル停止中のため、リクエストを承ることができません',
    '70017': '指定された銘柄は注文停止中のため、リクエストを承ることができません',
    '70018': '指定された銘柄は注文およびキャンセル停止中のため、リクエストを承ることができません',
    '70019': '注文はキャンセル中です',
    '70020': '現在成行注文停止中のため、注文を承ることができません',
    '70021': '指値注文価格が乖離率を超過しています',
    '70022': '現在逆指値指値注文停止中のため、注文を承ることができません',
    '70023': '現在逆指値成行注文停止中のため、注文を承ることができません'
}
